/**
 * $RCSfile$
 * $Revision: 11388 $
 * $Date: 2009-11-08 18:26:55 -0600 (Sun, 08 Nov 2009) $
 *
 * Copyright (C) 2003-2008 Jive Software. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.jawa.core.util;

import java.security.Security;
import java.text.SimpleDateFormat;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.Properties;

import javax.mail.Address;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.Part;
import javax.mail.SendFailedException;
import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.URLName;
import javax.mail.internet.AddressException;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeBodyPart;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeMultipart;
import javax.mail.internet.MimeUtility;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * A service to send email.<p>
 *
 * This class has a few factory methods you can use to return message objects
 * or to add messages into a queue to be sent. Using these methods, you can
 * send emails in the following couple of ways:<p>
 * <pre>
 *   EmailService.sendMessage(
 *     "Joe Bloe", "jbloe@place.org",
 *     "Jane Doe", "jane@doe.com",
 *     "Hello...",
 *     "This is the body of the email...",
 *     null
 *   );
 * </pre>
 * or
 * <pre>
 *   Message message = EmailService.createMimeMessage();
 *   // call setters on the message object
 *   // .
 *   // .
 *   // .
 *   emailService.sendMessage(message);
 * </pre><p>
 *
 * This class is configured with a set of Jive properties:<ul>
 *      <li><tt>mail.smtp.host</tt> -- the host name of your mail server, i.e.
 *          mail.yourhost.com. The default value is "localhost".
 *      <li><tt>mail.smtp.port</tt> -- an optional property to change the smtp
 *          port used from the default of 25.
 *      <li><tt>mail.smtp.username</tt> -- an optional property to change the
 *          username used to connect to the smtp server. Default is no username.
 *      <li><tt>mail.smtp.password</tt> -- an optional property to change the
 *          password used to connect to the smtp server. Default is no password.
 *      <li><tt>mail.smtp.ssl</tt> -- an optional property to set whether to use
 *          SSL to connect to the smtp server or not. Default is false.
 *      <li><tt>mail.debugEnabled</tt> -- true if debug information should written out.
 *          Default is false.
 * </ul>
 */
public class EmailService {

	private static final Logger Log = LoggerFactory.getLogger(EmailService.class);

	private static final String SSL_FACTORY = "org.jivesoftware.util.SimpleSSLSocketFactory";

	private static EmailService instance = new EmailService();

	public static EmailService getInstance() {
		return instance;
	}

	private String host;
	private int port;
	private String username;
	private String password;
	private boolean sslEnabled;
	private boolean debugEnabled;

	private Session session = null;

	/**
	 * Constructs a new EmailService instance.
	 */
	private EmailService() {
		host = JawaGlobals.getProperty("mail.smtp.host", "localhost");
		port = JawaGlobals.getIntProperty("mail.smtp.port", 25);
		username = JawaGlobals.getProperty("mail.smtp.username");
		password = JawaGlobals.getProperty("mail.smtp.password");
		sslEnabled = JawaGlobals.getBooleanProperty("mail.smtp.ssl");
		debugEnabled = JawaGlobals.getBooleanProperty("mail.debug");
	}

	/**
	 * Factory method to return a blank JavaMail message. You should use the
	 * object returned and set desired message properties. When done, pass the
	 * object to the addMessage(Message) method.
	 *
	 * @return a new JavaMail message.
	 */
	public MimeMessage createMimeMessage() {
		if (session == null) {
			createSession();
		}
		return new MimeMessage(session);
	}

	/**
	 * Sends a JavaMail message. To create a message, use the
	 * {@link #createMimeMessage()} method.
	 *
	 * @param message the message to send.
	 */
	public void sendMessage(MimeMessage message) {
		if (message != null) {
			sendMessages(Collections.singletonList(message));
		} else {
			Log.error("Cannot add null email message to queue.");
		}
	}

	/**
	 * Send a collection of messages. To create a message, use the
	 * {@link #createMimeMessage()} method.
	 *
	 * @param messages a collection of the messages to send.
	 */
	public void sendMessages(Collection<MimeMessage> messages) {
		// If there are no messages, do nothing.
		if (messages.size() == 0) {
			return;
		}
		TaskEngine.getInstance().submit(new EmailTask(messages));
	}

	/**
	 * Sends a message, specifying all of its fields.<p>
	 *
	 * To have more advanced control over the message sent, use the
	 * {@link #sendMessage(MimeMessage)} method.<p>
	 *
	 * Both a plain text and html body can be specified. If one of the values is null,
	 * only the other body type is sent. If both body values are set, a multi-part
	 * message will be sent. If parts of the message are invalid (ie, the toEmail is null)
	 * the message won't be sent.
	 *
	 * @param toName the name of the recipient of this email.
	 * @param toEmail the email address of the recipient of this email.
	 * @param fromName the name of the sender of this email.
	 * @param fromEmail the email address of the sender of this email.
	 * @param subject the subject of the email.
	 * @param textBody plain text body of the email, which can be <tt>null</tt> if the
	 *      html body is not null.
	 * @param htmlBody html body of the email, which can be <tt>null</tt> if the text body
	 *      is not null.
	 */
	public void sendMessage(String toName, String toEmail, String fromName, String fromEmail, String subject,
			String textBody, String htmlBody) {
		// Check for errors in the given fields:
		if (toEmail == null || fromEmail == null || subject == null || (textBody == null && htmlBody == null)) {
			Log.error("Error sending email: Invalid fields: " + ((toEmail == null) ? "toEmail " : "")
					+ ((fromEmail == null) ? "fromEmail " : "") + ((subject == null) ? "subject " : "")
					+ ((textBody == null && htmlBody == null) ? "textBody or htmlBody " : ""));
		} else {
			try {
				String encoding = MimeUtility.mimeCharset("UTF-8");
				MimeMessage message = createMimeMessage();
				Address to;
				Address from;

				if (toName != null) {
					to = new InternetAddress(toEmail, toName, encoding);
				} else {
					to = new InternetAddress(toEmail, "", encoding);
				}

				if (fromName != null) {
					from = new InternetAddress(fromEmail, fromName, encoding);
				} else {
					from = new InternetAddress(fromEmail, "", encoding);
				}

				// Set the date of the message to be the current date
				SimpleDateFormat format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z", java.util.Locale.US);
				//                format.setTimeZone(Locale.getDefault());
				message.setHeader("Date", format.format(new Date()));
				message.setHeader("Content-Transfer-Encoding", "8bit");
				message.setRecipient(Message.RecipientType.TO, to);
				message.setFrom(from);
				message.setSubject(subject, encoding);
				// Create HTML, plain-text, or combination message
				if (textBody != null && htmlBody != null) {
					MimeMultipart content = new MimeMultipart("alternative");
					// Plain-text
					MimeBodyPart text = new MimeBodyPart();
					text.setText(textBody, encoding);
					text.setDisposition(Part.INLINE);
					content.addBodyPart(text);
					// HTML
					MimeBodyPart html = new MimeBodyPart();
					html.setContent(htmlBody, "text/html; charset=UTF-8");
					html.setDisposition(Part.INLINE);
					html.setHeader("Content-Transfer-Encoding", "8bit");
					content.addBodyPart(html);
					// Add multipart to message.
					message.setContent(content);
					message.setDisposition(Part.INLINE);
					sendMessage(message);
				} else if (textBody != null) {
					MimeBodyPart bPart = new MimeBodyPart();
					bPart.setText(textBody, encoding);
					bPart.setDisposition(Part.INLINE);
					bPart.setHeader("Content-Transfer-Encoding", "8bit");
					MimeMultipart mPart = new MimeMultipart();
					mPart.addBodyPart(bPart);
					message.setContent(mPart);
					message.setDisposition(Part.INLINE);
					// Add the message to the send list
					sendMessage(message);
				} else if (htmlBody != null) {
					MimeBodyPart bPart = new MimeBodyPart();
					bPart.setContent(htmlBody, "text/html; charset=UTF-8");
					bPart.setDisposition(Part.INLINE);
					bPart.setHeader("Content-Transfer-Encoding", "8bit");
					MimeMultipart mPart = new MimeMultipart();
					mPart.addBodyPart(bPart);
					message.setContent(mPart);
					message.setDisposition(Part.INLINE);
					// Add the message to the send list
					sendMessage(message);
				}
			} catch (Exception e) {
				Log.error(e.getMessage(), e);
			}
		}
	}

	/**
	 * Sends a collection of email messages. This method differs from
	 * {@link #sendMessages(Collection)} in that messages are sent
	 * before this method returns rather than queueing the messages to be sent later.
	 *
	 * @param messages the messages to send.
	 * @throws MessagingException if an error occurs.
	 */
	public void sendMessagesImmediately(Collection<MimeMessage> messages) throws MessagingException {
		EmailTask task = new EmailTask(messages);
		task.sendMessages();
	}

	/**
	 * Returns the SMTP host (e.g. mail.example.com). The default value is "localhost".
	 *
	 * @return the SMTP host.
	 */
	public String getHost() {
		return host;
	}

	/**
	 * Sets the SMTP host (e.g. mail.example.com). The default value is "localhost".
	 *
	 * @param host the SMTP host.
	 */
	public void setHost(String host) {
		this.host = host;
		JawaGlobals.setProperty("mail.smtp.host", host);
		session = null;
	}

	/**
	 * Returns the port number used when connecting to the SMTP server. The default
	 * port is 25.
	 *
	 * @return the SMTP port.
	 */
	public int getPort() {
		return port;
	}

	/**
	 * Sets the port number that will be used when connecting to the SMTP
	 * server. The default is 25, the standard SMTP port number.
	 *
	 * @param port the SMTP port number.
	 */
	public void setPort(int port) {
		if (port < 0) {
			throw new IllegalArgumentException("Invalid port value: " + port);
		}
		this.port = port;
		JawaGlobals.setProperty("mail.smtp.port", Integer.toString(port));
		session = null;
	}

	/**
	 * Returns the username used to connect to the SMTP server. If the username
	 * is <tt>null</tt>, no username will be used when connecting to the server.
	 *
	 * @return the username used to connect to the SMTP server, or <tt>null</tt> if
	 *      there is no username.
	 */
	public String getUsername() {
		return username;
	}

	/**
	 * Sets the username that will be used when connecting to the SMTP
	 * server. The default is <tt>null</tt>, or no username.
	 *
	 * @param username the SMTP username.
	 */
	public void setUsername(String username) {
		this.username = username;
		if (username == null) {
			JawaGlobals.deleteProperty("mail.smtp.username");
		} else {
			JawaGlobals.setProperty("mail.smtp.username", username);
		}
		session = null;
	}

	/**
	 * Returns the password used to connect to the SMTP server. If the password
	 * is <tt>null</tt>, no password will be used when connecting to the server.
	 *
	 * @return the password used to connect to the SMTP server, or <tt>null</tt> if
	 *      there is no password.
	 */
	public String getPassword() {
		return password;
	}

	/**
	 * Sets the password that will be used when connecting to the SMTP
	 * server. The default is <tt>null</tt>, or no password.
	 *
	 * @param password the SMTP password.
	 */
	public void setPassword(String password) {
		this.password = password;
		if (password == null) {
			JawaGlobals.deleteProperty("mail.smtp.password");
		} else {
			JawaGlobals.setProperty("mail.smtp.password", password);
		}
		session = null;
	}

	/**
	 * Returns true if SMTP debugging is enabled. Debug information is
	 * written to <tt>System.out</tt> by the underlying JavaMail provider.
	 *
	 * @return true if SMTP debugging is enabled.
	 */
	public boolean isDebugEnabled() {
		return debugEnabled;
	}

	/**
	 * Enables or disables SMTP transport layer debugging. Debug information is
	 * written to <tt>System.out</tt> by the underlying JavaMail provider.
	 *
	 * @param debugEnabled true if SMTP debugging should be enabled.
	 */
	public void setDebugEnabled(boolean debugEnabled) {
		this.debugEnabled = debugEnabled;
		JawaGlobals.setProperty("mail.debug", Boolean.toString(debugEnabled));
		session = null;
	}

	/**
	 * Returns true if SSL is enabled for SMTP connections.
	 *
	 * @return true if SSL is enabled.
	 */
	public boolean isSSLEnabled() {
		return sslEnabled;
	}

	/**
	 * Sets whether the SMTP connection is configured to use SSL or not.
	 * Typically, the port should be 465 when using SSL with SMTP.
	 *
	 * @param sslEnabled true if ssl should be enabled, false otherwise.
	 */
	public void setSSLEnabled(boolean sslEnabled) {
		this.sslEnabled = sslEnabled;
		JawaGlobals.setProperty("mail.smtp.ssl", Boolean.toString(sslEnabled));
		session = null;
	}

	/**
	 * Creates a Javamail session.
	 */
	private synchronized void createSession() {
		if (host == null) {
			throw new IllegalArgumentException("Host cannot be null.");
		}

		Properties mailProps = new Properties();
		mailProps.setProperty("mail.smtp.host", host);
		mailProps.setProperty("mail.smtp.port", String.valueOf(port));
		// Allow messages with a mix of valid and invalid recipients to still be sent.
		mailProps.setProperty("mail.smtp.sendpartial", "true");
		mailProps.setProperty("mail.debug", String.valueOf(debugEnabled));

		// Methology from an article on www.javaworld.com (Java Tip 115)
		// We will attempt to failback to an insecure connection
		// if the secure one cannot be made
		if (sslEnabled) {
			// Register with security provider.
			Security.setProperty("ssl.SocketFactory.provider", SSL_FACTORY);

			mailProps.setProperty("mail.smtp.socketFactory.class", SSL_FACTORY);
			mailProps.setProperty("mail.smtp.socketFactory.fallback", "true");
		}

		// If a username is defined, use SMTP authentication.
		if (username != null) {
			mailProps.put("mail.smtp.auth", "true");
		}
		session = Session.getInstance(mailProps, null);
	}

	/**
	 * Task to send one or more emails via the SMTP server.
	 */
	private class EmailTask implements Runnable {

		private Collection<MimeMessage> messages;

		public EmailTask(Collection<MimeMessage> messages) {
			this.messages = messages;
		}

		public void run() {
			try {
				sendMessages();
			} catch (MessagingException me) {
				Log.error(me.getMessage(), me);
			}
		}

		public void sendMessages() throws MessagingException {
			Transport transport = null;
			try {
				URLName url = new URLName("smtp", host, port, "", username, password);
				if (session == null) {
					createSession();
				}
				transport = new com.sun.mail.smtp.SMTPTransport(session, url);
				transport.connect(host, port, username, password);
				for (MimeMessage message : messages) {
					// Attempt to send message, but catch exceptions caused by invalid
					// addresses so that other messages can continue to be sent.
					try {
						transport.sendMessage(message, message.getRecipients(MimeMessage.RecipientType.TO));
					} catch (AddressException ae) {
						Log.error(ae.getMessage(), ae);
					} catch (SendFailedException sfe) {
						Log.error(sfe.getMessage(), sfe);
					}
				}
			} finally {
				if (transport != null) {
					try {
						transport.close();
					} catch (MessagingException e) { /* ignore */
					}
				}
			}
		}
	}
}
